package cn.juque.luceneplus.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.juque.common.base.DataGrid;
import cn.juque.common.base.PageInfo;
import cn.juque.common.exception.AppException;
import cn.juque.luceneplus.constants.AnalyzerEnum;
import cn.juque.luceneplus.constants.BasicIndexFieldEnum;
import cn.juque.luceneplus.constants.FieldTypeEnum;
import cn.juque.luceneplus.constants.IndexEnum;
import cn.juque.luceneplus.constants.LuceneConfig;
import cn.juque.luceneplus.constants.LuceneMsgEnum;
import cn.juque.luceneplus.dto.FieldInfoDTO;
import cn.juque.luceneplus.dto.SearchAfterRequestDTO;
import cn.juque.luceneplus.dto.SearchAfterResultDTO;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Resource;
import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;
import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.Field.Store;
import org.apache.lucene.document.NumericDocValuesField;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.SortField.Type;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.store.Directory;
import org.springframework.stereotype.Service;

/**
 * @author nuoka
 * @version 1.0.0
 * <li>IntelliJ IDEA</li>
 * <li></li>
 * @date 2021/10/4 11:29
 **/
@Service("documentPlusService")
public class DocumentPlusService {

    @Resource
    private IndexPlusService indexPlusService;

    /**
     * 写入文档
     *
     * @param indexName 索引名称
     * @param params        参数值
     * @throws IOException IOException
     */
    public void addDocument(String indexName, Map<String, Object> params) throws IOException {
        FieldInfoDTO fieldInfoDTO = this.createFields(indexName, params);
        Map<String, Analyzer> analyzerMap = fieldInfoDTO.getAnalyzerMap();
        // 初始化writer
        PerFieldAnalyzerWrapper wrapper = new PerFieldAnalyzerWrapper(new StandardAnalyzer(), analyzerMap);
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(wrapper);
        Directory directory = this.indexPlusService.getDirectory(indexName);
        try (IndexWriter indexWriter = new IndexWriter(directory,indexWriterConfig)) {
            Document document = new Document();
            fieldInfoDTO.getFieldList().forEach(document::add);
            // 保存文档
            indexWriter.addDocument(document);
            indexWriter.commit();
        }
    }

    /**
     * 根据唯一标识更新文档
     * @param builder 更新文档的条件
     * @param params 参数
     */
    public void updateDocument(String indexName, BooleanQuery.Builder builder, Map<String, Object> params) throws IOException {
        List<Document> docList = CollUtil.newArrayList();
        SearchAfterRequestDTO searchAfterRequestDTO = new SearchAfterRequestDTO();
        searchAfterRequestDTO.setIndexName(indexName);
        searchAfterRequestDTO.setBuilder(builder);
        searchAfterRequestDTO.setPageSize(1000);
        SortField sortField = new SortField(IndexEnum.TIMESTAMP.getName(), Type.LONG);
        searchAfterRequestDTO.setSort(new Sort(sortField));
        boolean flag = true;
        SearchAfterResultDTO resultDTO;
        while (flag) {
            resultDTO = this.searchByAfter(searchAfterRequestDTO);
            if(CollUtil.isEmpty(resultDTO.getResult())) {
                flag = false;
                continue;
            }
            docList.addAll(resultDTO.getResult());
            searchAfterRequestDTO.setLastScoreDoc(resultDTO.getLastDoc());
        }
        // 删除
        this.deleteDocument(indexName, builder);
        // 新增
        Map<String, Object> mergeParams;
        for (Document doc : docList) {
            mergeParams = this.mergeFields(doc.getFields(), params);
            this.addDocument(indexName, mergeParams);
        }
    }

    /**
     * 删除文档
     * @param indexName 索引文档
     * @param builder 查询条件
     * @throws IOException IOException
     */
    public void deleteDocument(String indexName, BooleanQuery.Builder builder) throws IOException {
        Directory directory = this.indexPlusService.getDirectory(indexName);
        StandardAnalyzer standardAnalyzer = new StandardAnalyzer();
        IndexWriterConfig indexWriterConfig = new IndexWriterConfig(standardAnalyzer);
        try (IndexWriter indexWriter = new IndexWriter(directory, indexWriterConfig)) {
            indexWriter.deleteDocuments(builder.build());
            indexWriter.commit();
        }
    }

    /**
     * 获取一个文档
     * @param indexName 索引名称
     * @param builder 查询条件
     * @return Document
     * @throws IOException IOException
     */
    public Document searchDocument(String indexName, BooleanQuery.Builder builder) throws IOException {
        Directory directory = this.indexPlusService.getDirectory(indexName);
        try(IndexReader reader = DirectoryReader.open(directory)) {
            IndexSearcher searcher = new IndexSearcher(reader);
            TopDocs topDocs = searcher.search(builder.build(), 1);
            ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            if(scoreDocs.length <= 0) {
                return null;
            }
            return searcher.doc(scoreDocs[0].doc);
        }
    }

    /**
     * 查询实现逻辑分页分页
     *
     * @param indexName 索引名称
     * @param builder   查询条件
     * @param pageInfo  分页信息
     * @return DataGrid
     * @throws IOException IOException
     */
    public DataGrid<Document> searchByPage(String indexName, BooleanQuery.Builder builder, PageInfo pageInfo)
        throws IOException {
        Directory directory = this.indexPlusService.getDirectory(indexName);
        try (IndexReader indexReader = DirectoryReader.open(directory)) {
            IndexSearcher indexSearcher = new IndexSearcher(indexReader);
            TopDocs topDocs = indexSearcher.search(builder.build(), LuceneConfig.DOC_MAX_COUNT);
            ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            long totalHis = topDocs.totalHits.value;
            int start = (pageInfo.getPage() - 1) * pageInfo.getLimit();
            int end = start + pageInfo.getLimit();
            if (start > LuceneConfig.DOC_MAX_COUNT && start < totalHis) {
                throw new AppException(LuceneMsgEnum.SEARCH_PAGE_MAX_LIMIT_ERROR);
            }
            if (start > totalHis) {
                return new DataGrid<>(CollUtil.newArrayList(), totalHis);
            }
            List<Document> result = CollUtil.newArrayList();
            for (int i = start; i < end; i++) {
                if (i >= scoreDocs.length) {
                    break;
                }
                result.add(indexReader.document(scoreDocs[i].doc));
            }
            return new DataGrid<>(result, totalHis);
        }
    }

    /**
     * 滚动式分页查询
     *
     * @param requestDTO 入参
     * @return SearchAfterResultDTO
     */
    public SearchAfterResultDTO searchByAfter(SearchAfterRequestDTO requestDTO) throws IOException {
        Directory directory = this.indexPlusService.getDirectory(requestDTO.getIndexName());
        try(IndexReader indexReader = DirectoryReader.open(directory)) {
            IndexSearcher indexSearcher = new IndexSearcher(indexReader);
            TopDocs topDocs;
            if(null == requestDTO.getLastScoreDoc()) {
                topDocs = indexSearcher.search(requestDTO.getBuilder().build(), requestDTO.getPageSize(), requestDTO.getSort());
            } else {
                // 非第一页，则滚动式读取
                topDocs = indexSearcher.searchAfter(requestDTO.getLastScoreDoc(), requestDTO.getBuilder().build(), requestDTO.getPageSize(), requestDTO.getSort());
            }
            ScoreDoc[] scoreDocs = topDocs.scoreDocs;
            List<Document> docList = CollUtil.newArrayList();
            for (ScoreDoc scoreDoc : scoreDocs) {
                docList.add(indexReader.document(scoreDoc.doc));
            }
            ScoreDoc lastScoreDoc = scoreDocs.length > 0 ? scoreDocs[scoreDocs.length - 1] : requestDTO.getLastScoreDoc();
            SearchAfterResultDTO resultDTO = new SearchAfterResultDTO();
            if(null != lastScoreDoc) {
                resultDTO.setLastDoc(lastScoreDoc);
            }
            resultDTO.setResult(docList);
            resultDTO.setTotalHis(topDocs.totalHits.value);
            return resultDTO;
        }
    }

    /**
     * 根据定义的索引创建文档字段信息
     * @param indexName 索引名称
     * @param params 变量值
     * @return Document
     * @throws IOException IOException
     */
    private FieldInfoDTO createFields(String indexName, Map<String, Object> params) throws IOException {
        List<Document> fieldInfoList = this.indexPlusService.getIndexInfo(indexName);
        List<Field> fieldList = CollUtil.newArrayList();
        Map<String, Analyzer> analyzerMap = MapUtil.newHashMap();
        for (Document document : fieldInfoList) {
            String fieldName = document.get(BasicIndexFieldEnum.FIELD_NAME.getValue());
            if(params.containsKey(fieldName) && null != params.get(fieldName)) {
                String fieldType = document.get(BasicIndexFieldEnum.FIELD_TYPE.getValue());
                String fieldStore = document.get(BasicIndexFieldEnum.FIELD_STORE.getValue());
                String fieldPoint = document.get(BasicIndexFieldEnum.FIELD_POINT.getValue());
                String fieldSort = document.get(BasicIndexFieldEnum.FIELD_SORT.getValue());
                List<Field> subFieldList = FieldTypeEnum.forField(fieldName, params.get(fieldName), FieldTypeEnum.forName(fieldType), Store.valueOf(fieldStore), Boolean.valueOf(fieldPoint), Boolean.valueOf(fieldSort));
                fieldList.addAll(subFieldList);
                String fieldAnalyzer = document.get(BasicIndexFieldEnum.FIELD_ANALYZER.getValue());
                if(StrUtil.isNotEmpty(fieldAnalyzer)) {
                    analyzerMap.put(fieldName, AnalyzerEnum.forAnalyzer(fieldAnalyzer));
                }
            }
        }
        fieldList.add(new NumericDocValuesField(IndexEnum.TIMESTAMP.getName(), System.currentTimeMillis()));
        FieldInfoDTO fieldInfoDTO = new FieldInfoDTO();
        fieldInfoDTO.setFieldList(fieldList);
        fieldInfoDTO.setAnalyzerMap(analyzerMap);
        return fieldInfoDTO;
    }

    /**
     * 合并文档
     * @param sourceFields 文档
     * @param targetMap 新字段值
     */
    private Map<String, Object> mergeFields(List<IndexableField> sourceFields, Map<String, Object> targetMap) {
        Set<String> systemFields = CollUtil.newHashSet(IndexEnum.TIMESTAMP.getName());
        Map<String, Object> params = MapUtil.newHashMap();
        sourceFields.stream()
            .filter(t->!systemFields.contains(t.name()))
            .filter(t->!targetMap.containsKey(t.name())).forEach(t->params.put(t.name(), t.stringValue()));
        targetMap.forEach((key, value) -> {
            if(!systemFields.contains(key)) {
                params.put(key, value);
            }
        });
        return params;
    }
}
